最近最常收到的 RAG 現場描述是這句:
Top-K 少就常常答錯,拉到 20、30 才準一點。可是 context 變成兩三萬 token,問一題要等一分多鐘。
今天分兩半:先把「多塞一個 chunk 值幾秒」算成價目表,再說為什麼那個「只好多塞」通常不是模型需要那麼多 context。
模型本體的生成時間拆得開,而且只需要兩個數字:
模型推論時間 ≈ 輸入長度 ÷ pp(輸入長度) ← prefill
+ 輸出長度 ÷ tg(context 長度) ← decode
pp 是讀 prompt 的速度、tg 是吐答案的速度,兩者吃的硬體不同:Day 10 拆過 decode 那側通常更偏 memory-bound,prefill 的 token 共用同一份權重,相較之下更偏 compute-bound——但 context 一拉長,attention 與記憶體那本帳也會開始變重。兩個都是 context 的函數,不是常數——這正是今天要拆的第一件事。
⚠️ 用約等號有兩個理由。一是這條式子只涵蓋模型本體:真正的 E2E 還要加檢索、rerank、tokenize、取樣、排隊與 runtime overhead,llama-bench 連 tokenization 與 sampling 都不計,Day 02 那四把尺量的才是完整那一條。
二是下面第二項一律用 tg128 代估——那是 KV 幾乎空著時的 decode,所以每個總時間都偏樂觀。
所以「這台快不快」沒有單一答案,要看你餵多少、吐多少:
| 應用 | 送進模型的 input | output | 無 prefix cache 時 | 大部分 prefix 命中後 |
|---|---|---|---|---|
| 客服 FAQ | 2K–8K | 100–400 | 混合 | decode |
| 一般 chat | 2K–16K | 300–1K | 混合 | decode |
| 企業 RAG | 8K–40K | 300–1K | prefill/混合 | 混合 |
| 整份文件 QA | 30K–150K | 200–1K | prefill 強烈主導 | decode |
| coding autocomplete | 8K–64K | 10–200 | prefill 強烈主導 | cache 最重要 |
| 長 reasoning | 2K–20K | 2K–10K | decode | decode |
(欄位是量級不是量測,用你自己的 prompt_eval_count / eval_count 對一次就知道你在哪一列。)
RAG 那一列的 input 是浮動的,而最直接的那顆旋鈕,就是最後送進 LLM 的 Top-K。
先說死一件事:下面的 Top-K 指的是最後送進生成 LLM 的 chunk 數,不是檢索階段先撈回來的候選數。兩者的差別是後半篇的重點。
同一顆 Gemma 4 12B QAT、同一個 llama.cpp b10488、輸出固定 500 token,chunk 抓 800 token。⚠️ 為了把硬體曲線換成 Top-K,這裡忽略 system prompt、query 與 template 的固定開銷,實際服務要以 prompt_eval_count 為準:

圖 1:綠色那截從頭到尾一樣長,長出來的全是藍色。
| Top-8(6.4K) | Top-30(24K) | 多等 | |
|---|---|---|---|
| M4 Max 128GB · Metal | 24.2 s | 91.2 s | +67 s |
| RTX 4070 Ti · CUDA | 10.7 s | 18.7 s | +8 s |
⚠️ 6.4K 與 24K 沒有單獨跑測項,是拿相鄰實測點在 log₂(context) 軸上線性內插得到 pp(ctx) 再換算的——依實測曲線估算,不是直接量到的秒數(下面那兩個換邊點也是同一套方法)。
同一個決定,在這台 M4 Max 上是「多等一分鐘」,在這張 4070 Ti 上是「多等八秒」。最後送進 LLM 的那個 K 與 token 預算,是硬體與 latency SLO 相依的參數,不是抄來的預設值。
送進去的 token 只多 3.75 倍,M4 Max 的 prefill 卻多了 5.6 倍。差的那一截在這裡:

圖 2:越往右,兩台的 prefill 差距越大,而 Metal 這條掉得更兇。
pp 本身會隨 prompt 變長一路往下掉:T2 從 pp512 的 3794.74 掉到 pp32768 的 2169.27,少了 43%;同一段距離 T1 掉 59%。
機制上要小心一點:Gemma 4 12B 的 48 層並不是全部做 full attention,其中 40 層是 1K sliding window、只有 8 層是 full。前者的工作量比較接近 O(n×w),只有後者還帶著 O(n²) 那一項。context 一長,attention、記憶體存取與 kernel 效率一起把平均 pp 往下拖。
所以那條寫在便利貼上的除法(翻轉點 = 輸出長度 × pp ÷ tg)會估得太樂觀:它假設 pp 是常數。拿 pp512 一路外推,M4 Max 把換邊點估晚約 1.3 倍、4070 Ti 約 1.5 倍。改用逐點實測的 pp(ctx) 解,M4 Max 在 4,400 token 就換邊(大約 Top-6),4070 Ti 是 21,400(大約 Top-27)。
在這組 M4 Max + Gemma 4 12B 的帳上,RAG 幾乎一開始就在 prefill 的世界裡。
這裡是今天的重點。檢索階段的 K,和送進 LLM 的 K,本來就該是兩個 K,多數 pipeline 把它們綁在一起了:
可以先找很多 chunks,但不代表最後要把很多 chunks 全塞給 LLM。
「Top-K 少就答錯」的五種常見病因裡,只有一種真的是模型需要更多 context:
| 症狀 | 真正的病因 | 該動的地方 |
|---|---|---|
| 型號、料號、錯誤碼問不準 | dense embedding 分不開 PXR-450 與 PXR-400 |
BM25 + dense 用 RRF 融合 |
| Top-1 錯、Top-3 才對 | chunk 邊界把一份完整證據切爛 | 依章節/步驟/表格切,別固定 512 token |
| 表格、圖說裡的答案永遠找不到 | 轉 Markdown 時欄位、圖號、表頭掉了 | 表格另存 row-level 表示;保留 metadata |
| 命中了但上下文不足 | 只送命中的那一小塊 | parent/neighbor 展開:搜小的、送大的 |
| 證據夠了還是亂講 | hallucination | 拒答門檻 + 逐句對證據 verify |
前四種都發生在生成模型之前。拿 Top-K 當藥吃,只是把 ingestion、chunking、retrieval 或 context construction 的問題一起埋進 prefill 帳單裡。第四種確實要更多 context,但它要的是「一個完整章節」,不是三十個彼此重複的 chunk。

圖 3:候選撈得多不會膨脹主 LLM 的 prompt,貴的是最後送進去的那一段。
K_retrieve 是拿 recall 買的;K_context 是拿 prefill 延遲買的。
⚠️ 所以上線時控的應該是 token 預算,不是 chunk 數。先定一個目標(例如 6K–10K),rerank 後照分數挑命中點、補齊它的父章節,塞到預算為止;只有跨章節的綜合題才准超過。這樣圖 2 那條往上飛的曲線才有天花板。
至於「答錯到底是哪一層的錯」,那是另一整篇的份量,Day 22 會一層一層拆。
**這是紙上帳,不是端到端實測。**兩段各自量再相加,中間沒有排隊、取樣、載入,也沒讓 prefix cache 幫忙扣掉 prefill(明天的主題)。
T1 的 pp 在長 prompt 端不穩(process 內標準差 pp16384 ±14%、pp32768 ±12%),那個 +67 秒代進誤差是 +58 到 +79 秒;4070 Ti 那欄的 ± 在 0.15% 以內。⚠️ 跑 benchmark 前要清場——我第一輪就是機器上還有別的東西在跑,長 prompt 端整整低估了一到兩成。
還有一格要自己確認:掃到 32K 之前先過 Day 09 那條容量線。Gemma 4 的 40 層 sliding KV 在 1K window 封頂在 320 MiB,@32K 加上 8 層 global 的 512 MiB,全部 KV 只有 0.81 GiB,12GB 卡吃得下。那 40 層若維持同樣的 8 KV heads × 256、卻也改成 32K 全域,光這 40 層就膨脹到 10 GiB;再加原本 8 層 global,全部 KV 約 10.5 GiB——加上權重直接出局。
一台機器就能量出自己的價目表:
B=~/gguf-lab/bin-b10488/llama-b10488/llama-bench
M=~/gguf-lab/day08/gemma-4-12b-it-qat-q4_0.gguf
# Mac:每個測項各自一個 process、中間冷卻 240 秒(Day 11 驗過 240 秒就能恢復)
for P in 512 2048 8192 16384 32768; do
"$B" -m "$M" -p $P -n 0 -ngl 99 -fa off -r 3 -o md
[ "$P" = 32768 ] || sleep 240
done
"$B" -m "$M" -p 0 -n 128 -ngl 99 -fa off -r 5 -o md
# T2 這組測試沒觀察到明顯跑序效應,一行跑完即可
"$B" -m "$M" -p 512,2048,8192,16384,32768 -n 0 -ngl 99 -fa off -r 3 -o md
⚠️ 不要把五個 -p 串成一行在 Mac 上跑——那是同一個 process 連著燒,量到的是跑熱之後的曲線,不是上面那張圖。跑之前也先確認機器沒有其他重負載(我第一輪就栽在這裡)。
拿到 pp(ctx) 與 tg128,把「你的 Top-K × chunk 大小 ÷ pp」跟「輸出長度 ÷ tg」畫在同一條軸上,交點就是你那台的換邊點。
pp、tg 都是 context 的函數(檢索、排隊與 tokenize 還在外面)。先用你自己的 prompt_eval_count 對號入座,再決定要救哪一段:同一台機器,8K 時砍輸出有用,24K 時砍輸出幾乎沒感覺。pp 自己會隨 context 往下掉。prompt_tokens;送進去的只留 3–8 個命中點加上補齊的父章節,然後控 token 預算而不是控 chunk 數。明天 Day 18 動 prefill 這一側最大的一刀:prefix cache。算過的 KV 留著,下一個請求只要前綴相同就能跳過那段——但「相同」是逐 token 的嚴格比對,從第一個不同的 token 往後就不能沿用。
一個塞在前綴前段的時間戳,就足以讓後面幾萬 token 全部 miss,而且什麼錯都不會報。
咱們明天見。